<?php

class MSXTape {
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ 	*/
	/* MSX TAPE class - Reads and writes on a virtual tape file (.CAS)					*/
	/* Version 1.0																		*/
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ 	*/
	
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ 	*/
	/* GLOBAL PROPERTIES DECLARATION													*/
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ 	*/
	
	const MSX_FILE_NOT_FOUND = 0;
	const MSX_BINARY_FILE = 1;
	const MSX_BASIC_FILE = 2;
	const MSX_ASCII_FILE = 3;
	const MSX_CUSTOM_BLOCK = 4;
	
	protected $HEADER;
						
	protected $DECLARE_BINARY_FILE;
	protected $DECLARE_BASIC_FILE;
	protected $DECLARE_ASCII_FILE;
	
	protected $G_nFilePos;
	protected $G_nFileType;
	
	protected $nOrder;
	protected $cTitle;
	protected $nType;
	protected $nBegin;
	protected $nEnd;
	protected $nStart;
	protected $nSize;
	protected $cData;
	protected $lDeleted;
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	public function __construct() {
		
		/* Il costruttore della classe, che inizializza il valore di alcune variabili utilizzate
		 * all'interno della classe. Ad esempio G_Intestazione che contiene la serie di bytes (8)
		 * utilizzata nel formato CAS per inserire un segnale di sincronizzazione per l'MSX
		 * (Quella cosa che ascoltando l'audio all'inizio di un file fa BIIIIIIIIIIIIIIIIIIIIP...
		 * Poi DECLARE_BINARY_FILE, Basic e ASCII che contengono i dieci byte che fanno capire nel
		 * file CAS se quello che segue è un File Binario, Basic o ASCII. */
		
		$this->HEADER  = chr(hexdec("1F")) . chr(hexdec("A6")) . chr(hexdec("DE"));
		$this->HEADER .= chr(hexdec("BA")) . chr(hexdec("CC")) . chr(hexdec("13"));
		$this->HEADER .= chr(hexdec("7D")) . chr(hexdec("74"));
		
		$this->DECLARE_BINARY_FILE = str_pad("", 10, chr(hexdec("D0")));
  		$this->DECLARE_BASIC_FILE  = str_pad("", 10, chr(hexdec("D3")));
  		$this->DECLARE_ASCII_FILE  = str_pad("", 10, chr(hexdec("EA")));
		
		/* Inizializza i vari array, per partire con una nuova cassetta */
		
		$this->newTape();
		
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	public function newTape() {
		
		/* Azzera gli array e di fatto crea una nuova cassetta */
		
	  	$nOrder = 0;
		$cTitle = null;
		$nType = 0;
		$nBegin = 0;
		$nEnd = 0;
		$nStart = 0;
		$nSize = 0;
		$cData = null;
		$lDeleted = false;
		
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	public function open($P_cFile) {
		
		$oFile = null;
		$cStream = "";
		$nValue = 0;
		
		//$P_cFile = $dir_path = str_replace( $_SERVER['DOCUMENT_ROOT'], "", dirname(realpath(__FILE__)) ) . DIRECTORY_SEPARATOR . $P_cFile; 
		
		if(file_exists($P_cFile)) {
			
			$oFile = fopen($P_cFile, "rb");			
			while (!feof($oFile)) $cStream .= fread($oFile, 8192);
			fclose($oFile);	
			
			$nValue = $this->openStream($cStream);
			
		}
		
		return $nValue;
	
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	public function openStream($P_cStream) {
		
		/* Scompone un file CAS in tanti files che andranno negli array */
		
		$nPos1 = 0;
		$nPos2 = 0;
		$nPos3 = 0;
		$nHeaderCount = 0;
		$nTypeFile = self::MSX_FILE_NOT_FOUND;
		$cTemp = "";
		$cSlice = "";
		$cSubSlice = "";
		$lKeepSearching = true;
		$lCercaSottoporzioni = false;
		$lFirstHeaderFound = False;
		
		/* Cerca la prima intestazione */
		
		$nPos1 = $this->seekHeader($P_cStream);
		
  		while ( ($nPos1 !== false) && ($lKeepSearching) ) { // || ( ! lIntestazioneInizialeTrovata ) ) {
			
			$lFirstHeaderFound = true;
			
			/* Cerca la posizione dell'intestazione successiva
			 * Con il parametro finale di CercaIntestazione settato su True,
			 * gli dico di restituirmi sempre e comunque un valore, anche se 
			 * non trova un'intestazione (mi da la fine del file in questo caso) */
			
			//echo "nPos1 = " . $nPos1 . "<br/>";
			$nPos2 = $this->seekHeader($P_cStream, $nPos1 + 1, true);
			//echo "nPos2 = " . $nPos2 . "<br/>";
			if($nPos2 === false) $nPos2 = strlen($P_cStream);
			
			if ( $nPos2 !== false ) {
				
				/* Tutto quello che sta nel mezzo fra un'intestazione e l'altra 
				 * è il corpo del file da catalogare */
				
				$cSlice = substr($P_cStream, $nPos1, $nPos2 - $nPos1 + 1);
				$cSubSlice = "";
      			
      			$nTypeFile = $this->returnBlockType($cSlice);
				if ($nTypeFile == self::MSX_BINARY_FILE) {
				
					/* A questo punto sta trattando un file binario. Parte dal presupposto 
					* che dei blocchi custom di codice devono essere preceduti da almeno un 
					* file binario che faccia da "loader". Non è sempre detto che sia così...
					* soprattutto con alcuni lati B delle cassette che contengono solo
					* blocchi custom */
					
					$lSeekSubSlices = true;
					$nHeaderCount = 0;
					$nPos3 = 0;
					$nPos3 = strpos($cSlice, $this->HEADER, $nPos3);
					
					while ( ($nPos3 !== false) && ($lSeekSubSlices) ) {
						
						$nHeaderCount++;
						
						if ($nHeaderCount > 2) {
							$cSubSlice = substr($cSlice, $nPos3, strlen($cSlice) - $nPos3 + 1);
							$cSlice = substr($cSlice, 0, $nPos3 - 1);
							$lSeekSubSlices = false;
						} else {
							$nPos3++;
							$nPos3 = strpos($cSlice, $this->HEADER, $nPos3);
						} /* End If */
					
					} /* End While */
					
					$a = $this->returnBlockType($cSlice);
					$this->addBlock($cSlice);
					$a = $this->returnBlockType($cSlice);
					
					if ($cSubSlice != "") {
        				
						/* C'è anche una sezione di custom data da aggiungere */
						
						$this->addBlock($cSubSlice);
						
					} /* End If */
					
      			} else {
					
					/* Non è un file binario, quindi lo aggiunge senza chiedersi se 
					* in mezzo al file ci siano o meno dei blocchi custom */
					
					$this->addBlock($cSlice);
					
				} /* End If */
					
				/* Se è arrivato al termine del file, si ricorda di terminare l'analisi una
				 * volta concluso il ciclo */
				 
				if ($nPos2 >= strlen($P_cStream)) $lKeepSearching = false;
				
			} else {
				
				$lKeepSearching = false;
				
			} /* End If */
				
			/* Cerca la prossima intestazione da analizzare */
				
			$nPos1 = $this->seekHeader($P_cStream, $nPos2);
			
		} /* End While */
			
			
		return $lFirstHeaderFound;
		
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	private function seekHeader(&$P_cStream = "", $P_nStartPos = 0, $lAlwaysReturnValue = false) {
		
		/* Cerca all'interno del file .CAS l'intestazione del file */
		
		$nPos = 0;
		$cTemp = "";
		$nStartingHeaderPos = 0;
  		$lHeaderFound = false;
		$nType = self::MSX_FILE_NOT_FOUND;
  		
		$nPos = strpos($P_cStream, $this->HEADER, $P_nStartPos);
		
		while ( ($nPos !== false) && ( !$lHeaderFound ) ) {
			
			/* Se trova l'intestazione, prova a cercare l'altra parte di header che 
			 * segnala se il file che segue è un binario, basic o ASCII */
			
			$nType = $this->returnBlockType($P_cStream, $nPos);
			
			if ( ( $nType != self::MSX_FILE_NOT_FOUND) && ($nType != self::MSX_CUSTOM_BLOCK) ) {
      			
				$nStartingHeaderPos = $nPos;
      			$lHeaderFound = true;
				
			} else {				
				
				$nPos = strpos($P_cStream, $this->HEADER, $nPos + 1);
				
			} /* End If */
			
		} /* End While */
		
		if ( !$lHeaderFound ) {
			
			/* Se non ha trovato una nuova intestazione, decide il da farsi a seconda del 
			 * parametro lRestituisciComunqueValore che gli è stato passato... ritorna come 
			 * valore 0 oppure la lunghezza massima del file, se è necessario */
			
			if ( $lAlwaysReturnValue ) { 
				$nStartingHeaderPos = strlen($P_cStream);
			} else {
      			$nStartingHeaderPos = 0;
			} /* End If */
			
		} /* End If */
  
		/* Inserisce in queste due variabili la posizione in bytes in cui si trova  
		 * l'intestazione ed il tipo del file trovato 
		 * (mi tocca farlo perchè non posso restituire due variabili) */
		
		$this->G_nFilePos = $nStartingHeaderPos;
 		$this->G_nFileType = $nType;
		
  		return $this->G_nFilePos;
		
	}

	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	private function returnBlockType(&$P_cStream, $P_nStartPos = 0) {
		
		/* Restituisce il tipo di file */
		
		$cTemp = "";
  		$nType = self::MSX_FILE_NOT_FOUND;
		
		/* Nello standard MSX il tipo di file viene indicato con un carattere ripetuto 
		 * per 10 volte subito dopo il segnale di sincronismo (il BIIIIIP) per cui è 
		 * necessario che prenda i 10 caratteri successivi a quello di partenza e li 
		 * analizzi */
		
		if ( ($P_nStartPos + strlen($this->HEADER) + 10) < strlen($P_cStream) ) {
			$cTemp = substr($P_cStream, $P_nStartPos + strlen($this->HEADER), 10);
		} /* End If */
    
		switch ($cTemp) {
			
			case ($this->DECLARE_BINARY_FILE):
				$nType = self::MSX_BINARY_FILE;
				break;
  			case ($this->DECLARE_BASIC_FILE):
				$nType = self::MSX_BASIC_FILE;
				break;
			case ($this->DECLARE_ASCII_FILE):
				$nType = self::MSX_ASCII_FILE;
				break;
			default:
				$nType = self::MSX_CUSTOM_BLOCK;
				
		} /* End Case */
		
		return $nType;
		
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	private function returnBlockTitle(&$P_cStream) {
		
		return substr($P_cStream, strlen($this->HEADER) + 10, 6);
		
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	private function addBlock($P_cStream) {
		
		/* Aggiunge un nuovo file agli array */
  		
		$nInd = 0;
		$nPos1 = 0;
		$nPos2 = 0;
		$cSlice = "";
		
		if($this->cTitle === null) {
			
			/* Azzera gli array e di fatto crea una nuova cassetta */
  			
			$nMaxBlocks = 0;
			
		  	$nOrder = array();
			$cTitle = array();
			$nType = array();
			$nBegin = array();
			$nEnd = array();
			$nStart = array();
			$nSize = array();
			$cData = array();
			$lDeleted = array();
			
		} else {
			
			$nMaxBlocks = count($this->cTitle);
			
			array_push($this->nOrder, 0);
			array_push($this->cTitle, "");
			array_push($this->nType, self::MSX_FILE_NOT_FOUND);
			array_push($this->nSize, 0);
			array_push($this->cData, "");
			array_push($this->lDeleted, false);			
			
		}
		
  		if ($this->returnBlockType($P_cStream) != self::MSX_CUSTOM_BLOCK) {
			
			/* Non è un blocco di dati custom, per cui SICURAMENTE lo può 
			 * trattare come un unico file da aggiungere all'array
			 * Vede qual'è l'ultimo elemento dell'array, per appendere di seguito 
			 * il nuovo record */
			
			$this->nOrder[$nMaxBlocks] = $nMax;
			$this->lDeleted[$nMaxBlocks] = false;
			$this->nType[$nMaxBlocks] = $this->returnBlockType($P_cStream);
			if ( ( $this->nType[$nMaxBlocks] != self::MSX_CUSTOM_BLOCK )
				&& ($this->nType[$nMaxBlocks] != self::MSX_FILE_NOT_FOUND) ) {
				$this->cTitle[$nMaxBlocks] = $this->returnBlockTitle($P_cStream);
			} else {
				$this->cTitle[$nMaxBlocks] = "";				
			} /* End If */
			
			$this->cData[$nMaxBlocks] = $P_cStream;
			$this->nSize[$nMaxBlocks] = strlen($this->cData[$nMaxBlocks]) - 33;
			
		} else {
			
			/* E' un blocco di dati custom, per cui è costretto ad analizzarlo per 
			 * capire se ci siano uno o più sottoblocchi da aggiungere all'array */
			
   			/* Vede qual'è l'ultimo elemento dell'array, per appendere di seguito 
   			 * il nuovo record */
			
			$nPos1 = strpos($P_cStream, $this->HEADER, 0);
			
			while ($nPos1 !== false) {
				
				/* Cerca la posizione dell'intestazione successiva. Con il parametro 
				 * finale di CercaIntestazione settato su True, gli dico di restituirmi
				 * sempre e comunque un valore, anche se non trova un'intestazione
				 * (mi da la fine del file in questo caso) */
				
				$nPos2 = strpos($P_cStream, $this->HEADER, $nPos1 + 1);
				
      			if ($nPos2 !== false) {
					
					/* Tutto quello che sta nel mezzo fra un'intestazione e l'altra è 
					 * il corpo del file da catalogare */
					
					$cSlice = substr($P_cStream, $nPos1, $nPos2 - $nPos1 + 1);
					$nPos1 = $nPos2;
					
				} else {
					
					$cSlice = substr($P_cStream, $nPos1, strlen($P_cStream) - $nPos1 + 1);
					$nPos1 = false;
					
				} /* End If */
      			
				$this->nOrder[$nMaxBlocks] = $nMaxBlocks;
				$this->lDeleted[$nMaxBlocks] = false;
				$this->nType[$nMaxBlocks] = self::MSX_CUSTOM_BLOCK;
      			$this->cTitle[$nMaxBlocks] = "";
				
				$this->cData[$nMaxBlocks] = $cSlice;
				$this->nSize[$nMaxBlocks] = strlen($this->cData[$nMaxBlocks]) - 9;
				
      			$nMaxBlocks = count($this->cTitle);
				
				array_push($this->nOrder, 0);
				array_push($this->cTitle, "");
				array_push($this->nType, self::MSX_FILE_NOT_FOUND);
				array_push($this->nSize, 0);
				array_push($this->cData, "");
				array_push($this->lDeleted, false);			
				
			} /* End While */
			
			if ($this->cTitle !== null) {
				array_pop($this->nOrder);
				array_pop($this->cTitle);
				array_pop($this->nType);
				array_pop($this->nSize);
				array_pop($this->cData);
				array_pop($this->lDeleted);			
			}
			
		} /* End If */
		
	} /* End Function */
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
	public function XMLData() {
		
		$nInd = 0;
		$xmlDoc = new SimpleXMLElement("<tape></tape>");
		$xmlTape = null;
		$xmlBlock = null;
		
		$xmlDoc->addAttribute("count", (count($this->cTitle)) );
		
		while($nInd < count($this->cTitle)) {
			
			/* Scrive qualcosa sullo schermo */
			
			$xmlBlock = $xmlDoc->addChild("block");
			
			if($this->cTitle[$nInd] != "") $xmlBlock->addChild("title", trim($this->cTitle[$nInd]));
			
			switch ($this->nType[$nInd]) {
				case self::MSX_BINARY_FILE:
					$xmlBlock->addChild("type", "binary");
					break;
				case self::MSX_BASIC_FILE:
					$xmlBlock->addChild("type", "basic");
					break;
				case self::MSX_ASCII_FILE:
					$xmlBlock->addChild("type", "ascii");
					break;
				case self::MSX_CUSTOM_BLOCK:
					$xmlBlock->addChild("type", "custom");
					break;
			}
			$xmlBlock->addChild("length", ($this->nSize[$nInd]));
			
			$nInd++;
			
		}
		
		return $xmlDoc;
		
	}
	
	/* _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ _-=-_-=-_ */
	
}


/* This tiny piece of code is needed only for testing purposes */
/*
$t1 = microtime();
$pippo = new MSXTape();
$pippo->open("tapes/PACLAND.CAS");
$pluto = $pippo->XMLData();

echo("INSIDE THIS TAPE:<br/><br/>");
foreach($pluto->block as $block) {
	if($block->title != "") echo(chr(34) . $block->title . chr(34) . " (" . $block->type . ")");
	else echo("Custom Block");
	echo(" (" . $block->length . " bytes)<br/>");
}
//file_put_contents('output.xml', $pluto->asXML());
$t2 = microtime() - $t1;
echo("<br/><br/>Elapsed time: " . $t2);
*/
echo "Finito";
?>